Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search for a trusted host in ProxyHeadersMiddleware #591

Merged
merged 4 commits into from
Mar 13, 2021

Conversation

b0g3r
Copy link
Contributor

@b0g3r b0g3r commented Mar 17, 2020

Fixes #590

If we have * in trusted proxies we should return the first host in the X-Forwarded-For as the client address.

In other cases, we should iterate over X-Forwarded-For hosts backward and return the first host which isn't included in trusted proxies

@b0g3r
Copy link
Contributor Author

b0g3r commented Mar 22, 2020

I found a discussion about this feature: 723d3dc#r36079732

So, I'll try to find examples from other servers with this feature and make comparison

else:
self.trusted_hosts = trusted_hosts
self.trusted_hosts = set(trusted_hosts)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For speedup str not in collection operations

@b0g3r b0g3r changed the title Fix ProxyHeadersMiddleware, now it grabs the first IP from x-forwarded-for Add search for a trusted host in ProxyHeadersMiddleware Mar 22, 2020
Copy link

@iamlordaubrey iamlordaubrey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my understanding, the idea here is to return the actual client ip address. This as it is returns the last ip address (which could be a proxy address)

if self.always_trust:
return x_forwarded_for_hosts[0]

for host in reversed(x_forwarded_for_hosts):
Copy link
Contributor Author

@b0g3r b0g3r Apr 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a security feature: we should return only the last non-trusted host.

Trivial case:

  • Proxy-server A
  • Proxy-server B
  • Client C
  • We trust both of proxies (trusted_hosts = A, B) or trust every proxy (trusted_hosts = *)

So in this scenario, a header will be X-Forwarded-For: C, B, A and we can securely extract C as a trusted client address.

Unfortunately, IRL we have a situation when we have a chain of proxies, and we can trust only a few of them, e.g:

  • Proxy-server A
  • Proxy-server B
  • Client C
  • We trust only A (trusted_hosts = A)

In this case, non-trusted proxy-server B can replace the real client address, so we should return the first non-trusted host and it will be B. Because the chain is actually reverted, we should check it also with reversed.

tl;dr: In a simple case, we can trust all of the proxies with trusted_hosts = * and it will return real client address (the first in X-Forwarded-For)

if self.always_trust:
return x_forwarded_for_hosts[0]

for host in reversed(x_forwarded_for_hosts):
Copy link
Contributor Author

@b0g3r b0g3r Apr 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a security feature: we should return only the last non-trusted host.

Trivial case:

  • Proxy-server A
  • Proxy-server B
  • Client C
  • We trust both of proxies (trusted_hosts = A, B) or trust every proxy (trusted_hosts = *)

So in this scenario, a header will be X-Forwarded-For: C, B, A and we can securely extract C as a trusted client address.

Unfortunately, IRL we have a situation when we have a chain of proxies, and we can trust only a few of them, e.g:

  • Proxy-server A
  • Proxy-server B
  • Client C
  • We trust only A (trusted_hosts = A)

In this case, non-trusted proxy-server B can replace the real client address, so we should return the first non-trusted host and it will be B. Because the chain is actually reverted, we should check it also with reversed.

tl;dr: In a simple case, we can trust all of the proxies with trusted_hosts = * and it will return real client address (the first in X-Forwarded-For)

@iamlordaubrey
Copy link

iamlordaubrey commented Apr 2, 2020

From my understanding, the idea here is to return the actual client ip address. This as it is returns the last ip address (which could be a proxy address)

Ok, I think I get it now. Thanks

@ghost ghost mentioned this pull request May 26, 2020
@rlittlefield
Copy link

This change is working well for me. The old way of specifying N proxies worked better for my use case because google cloud load balancers + kubernetes services always adds two hosts to the header, and I don't get to know what those are in advance. I can get this to work with your changes. Thank you!

@iamlordaubrey
Copy link

@tomchristie please can someone take a look at this PR? Sorry for the inconvenience 🙏

@ghost
Copy link

ghost commented Jun 10, 2020

Is there any chance to release this any time soon?
Slow API (https://pypi.org/project/slowapi/) can not work without this change.


for host in reversed(x_forwarded_for_hosts):
if host not in self.trusted_hosts:
return host
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a fall through case so there's no way we can end up returning None here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fail to see how it would be possible

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would happen if all the IPs in the chain were explicitly listed in trusted_hosts - but this would only occur if trusted_hosts were incorrectly set and/or the first reverse proxy in the chain failed to set x-forwarded-for to the client's IP (not our fault).

In this case, is there any other justifiable return value than None? I can't think of one

Copy link
Member

@euri10 euri10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, this would be a great addition, is there something we can do @tomchristie to alleviate your concerns on the above point ?

@dp-rufus
Copy link

dp-rufus commented Mar 1, 2021

@euri10 I'm not sure a fall-through case is necessary above - None might be the most sensible return value under the (unlikely) circumstances that would trigger this behaviour.

@tomchristie could you take another look at this PR please? I've just been bitten by this problem in the wild and it would be good to get this merged. Thanks

@euri10
Copy link
Member

euri10 commented Mar 2, 2021

updated it in https://github.com/encode/uvicorn/compare/master...euri10:b0g3r-uvicorn-590?expand=1 to solve conflicts because of the "new" test suite (original PR was done previous to the test suite being "revamped")

@b0g3r
Copy link
Contributor Author

b0g3r commented Mar 13, 2021

@euri10 Thank you for your comment!

I rebased this PR onto master and fix related issues (127.0.0.1 instead of testclient and others.) The last piece to merge is code review, right?

Copy link
Member

@euri10 euri10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just correct this small typo in the comment and we gtg I think, this is a great addition thanks @b0g3r

Co-authored-by: euri10 <[email protected]>
x_forwarded_for_hosts = [
item.strip() for item in x_forwarded_for.split(",")
]
host = self.get_trusted_client_host(x_forwarded_for_hosts)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the x-forwarded-for format is IP1:PORT1,IP2:PORT2,which is common in the L7 load balance? the port here is always set zero which is not reasonable.

Kludex pushed a commit to sephioh/uvicorn that referenced this pull request Oct 29, 2022
* Fix ProxyHeadersMiddleware, now it grabs the first IP from `x-forwarded-for`

Also rewrite a few tests for increasing coverage for edge cases

* Add searching algorithm for the trusted client host in x-forwarded-for

* Fix issues after rebasing

* Fix typo in comment

Co-authored-by: euri10 <[email protected]>

Co-authored-by: euri10 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proxy Middleware grabs the last IP in x-forwarded-for, not the first
7 participants